<!DOCTYPE html>
<title>Tests that GC preserves all of attached HTMLMediaElement+MSE when only part of that has a live reference.</title>
<script src="/w3c/resources/testharness.js"></script>
<script src="/w3c/resources/testharnessreport.js"></script>
<script>

function TestScope() {
    this.video = null;
    this.media_source = null;
    this.object_url = null;
    this.source_buffer_list = null;
    this.expect_durationchange = false;
    this.expect_sourceclose = false;
    this.received_sourceclose = false;
    this.expect_error = false;
    this.received_error = false;
}

function setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb) {
    // |handler_setup_cb|, if not null, is synchronously called after
    // test_scope is populated with HTMLME and MSE references, but prior to
    // attaching HTMLME to MSE API.
    //
    // Next, attaches the HTMLME to the MediaSource object and sets a timeout to later
    // call |setup_done_cb| so that hopefully any pending events or other work
    // are complete before the main part of the test proceeds.

    test_scope.video = document.createElement("video");
    test_scope.media_source = new MediaSource();
    test_scope.object_url = URL.createObjectURL(test_scope.media_source);
    if (handler_setup_cb != null)
        handler_setup_cb();

    test_scope.media_source.onsourceopen = test.step_func(function() {
        test_scope.media_source.onsourceopen = null;
        URL.revokeObjectURL(test_scope.object_url);
        setTimeout(setup_done_cb, 0);
    });

    test_scope.video.src = test_scope.object_url;
}

async_test(function(test) {
    var test_scope = new TestScope();

    var handler_setup_cb = null;

    var setup_done_cb = test.step_func(function() {
        test_scope.video = null;
        // |test_scope.media_source| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");

        // Debug note: setting test_scope.media_source to null here, and commenting out the assert_equals()
        // and test.done() lines, below, demonstrates with debug logging that HTMLME+MSE is collected, followed
        // eventually by a timeout.
        gc();

        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open after gc().");
        test.done();
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least MediaSource when only the MediaSource reference is held by JS");

async_test(function(test) {
    // This test builds on the previous test. It should fail if the previous test fails.

    var test_scope = new TestScope();

    var handler_setup_cb = test.step_func(function() {
        test_scope.video.ondurationchange = test.step_func(function() {
            assert_true(test_scope.expect_durationchange, "HTMLME durationchange event is expected only after changing MediaSource duration");
            test.done();
        });
    });

    var setup_done_cb = test.step_func(function() {
        test_scope.video = null;
        // |test_scope.media_source| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        gc();
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open after gc().");

        // Verify that HTMLME is still alive by changing the MediaSource object's duration, which causes
        // a durationchange event to become queued for dispatch on the attached HTMLME (and such dispatch ends this
        // test.)
        // Debug note: commenting out the next line should result in test timeout.
        test_scope.media_source.duration = 100;
        test_scope.expect_durationchange = true;
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least MediaSource and HTMLME when only the MediaSource reference is held by JS");

async_test(function(test) {
    // This test builds on the previous two tests. It should fail if either of the previous two tests fails.

    var test_scope = new TestScope();

    var handler_setup_cb = test.step_func(function() {
        test_scope.video.ondurationchange = test.step_func(function(e) {
            assert_true(test_scope.expect_durationchange, "HTMLME durationchange event is expected only after changing MediaSource duration");
            assert_equals(e.target.custom_test_wrapper_update, "testing", "HTMLME wrapper, as adjusted by the test, should be retained");
            test.done();
        });
    });

    var setup_done_cb = test.step_func(function() {
        // Update the HTMLME JS wrapper with a custom property to be verified later.
        test_scope.video.custom_test_wrapper_update = "testing";
        test_scope.video = null;
        // |test_scope.media_source| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        gc();
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open after gc().");

        // Verify that HTMLME is still alive by changing the MediaSource object's duration, which causes
        // a durationchange event to become queued for dispatch on the attached HTMLME (and such dispatch ends this
        // test.)
        // Debug note: commenting out the next line demonstrates with debug logging that HTMLME+MSE is collected,
        // followed eventually by a timeout.
        test_scope.media_source.duration = 100;

        test_scope.expect_durationchange = true;
        test_scope.media_source = null;
        gc();
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least HTMLME and its wrapper when no references held by JS, but there is a pending HTMLME event");

async_test(function(test) {
    var test_scope = new TestScope();

    var handler_setup_cb = null;

    var setup_done_cb = test.step_func(function() {
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        test_scope.media_source = null;
        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct before gc().");
        // |test_scope.video| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.

        // Debug note: setting test_scope.video to null here, and commenting out the assert_equals()
        // and test.done() lines, below, demonstrates with debug logging that HTMLME+MSE is collected, followed
        // eventually by a timeout.
        gc();

        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct after gc().");
        test.done();
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least HTMLME when only the HTMLME reference is held by JS");

async_test(function(test) {
    // This test builds on the previous test. It should fail if the previous test fails.

    var test_scope = new TestScope();

    var handler_setup_cb = test.step_func(function() {
        test_scope.media_source.onsourceclose = test.step_func(function() {
            assert_true(test_scope.expect_sourceclose, "MediaSource sourceclose event is expected only after clearing HTMLME src attribute");
            // Both HTMLME.onerror and MediaSource.onsourceclose are required to finish this test.
            if (test_scope.received_error)
               test.done();
            test_scope.received_sourceclose = true;
            test_scope.expect_sourceclose = false;  // Only one sourceclose is expected.
        });

        test_scope.video.onerror = test.step_func(function() {
            assert_true(test_scope.expect_error, "HTMLME error event is expected only after clearing HTMLME src attribute");
            // Both HTMLME.onerror and MediaSource.onsourceclose are required to finish this test.
            if (test_scope.received_sourceclose)
               test.done();
            test_scope.received_error = true;
            test_scope.expect_error = false;  // Only one error is expected.
        });
    });

    var setup_done_cb = test.step_func(function() {
        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        test_scope.media_source = null;
        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct before gc().");
        // |test_scope.video| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.

        gc();

        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct after gc().");

        // Verify that MediaSource is still alive by clearing the HTMLME object's src attribute, which causes a
        // sourceclose event to become queued for dispatch on the previously attached MediaSource and an error
        // event to become queued for dispatch on the HTMLME object (and such dispatches end this test.)
        // Debug note: commenting out the next line should result in test timeout.
        test_scope.video.src = "";
        test_scope.expect_sourceclose = true;
        test_scope.expect_error = true;
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least MediaSource and HTMLME when only the HTMLME reference is held by JS");

async_test(function(test) {
    // This test builds on the previous two tests. It should fail if either of the previous two tests fails.

    var test_scope = new TestScope();

    var handler_setup_cb = test.step_func(function() {
        test_scope.media_source.onsourceclose = test.step_func(function(e) {
            assert_true(test_scope.expect_sourceclose, "MediaSource sourceclose event is expected only after clearing HTMLME src attribute");
            assert_equals(e.target.custom_test_wrapper_update, "testing-mediasource", "MediaSource wrapper, as adjusted by the test, should be retained");
            // Both HTMLME.onerror and MediaSource.onsourceclose are required to finish this test.
            if (test_scope.received_error)
               test.done();
            test_scope.received_sourceclose = true;
            test_scope.expect_sourceclose = false;  // Only one sourceclose is expected.
        });

        test_scope.video.onerror = test.step_func(function(e) {
            assert_true(test_scope.expect_error, "HTMLME error event is expected only after clearing HTMLME src attribute");
            assert_equals(e.target.custom_test_wrapper_update, "testing-htmlme", "HTMLME wrapper, as adjusted by the test, should be retained");
            // Both HTMLME.onerror and MediaSource.onsourceclose are required to finish this test.
            if (test_scope.received_sourceclose)
               test.done();
            test_scope.received_error = true;
            test_scope.expect_error = false;  // Only one error is expected.
        });
    });

    var setup_done_cb = test.step_func(function() {
        // Update the HTMLME and MediaSource JS wrappers with custom properties to be verified later.
        test_scope.video.custom_test_wrapper_update = "testing-htmlme";
        test_scope.media_source.custom_test_wrapper_update = "testing-mediasource";

        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        test_scope.media_source = null;
        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct before gc().");
        // |test_scope.video| should be the only remaining reference to HTMLME+MSE.
        // GC shouldn't collect HTMLME+MSE due to that live reference.

        gc();

        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct after gc().");

        // Verify that MediaSource is still alive by clearing the HTMLME object's src attribute, which causes a
        // sourceclose event to become queued for dispatch on the previously attached MediaSource and an error
        // event to become queued for dispatch on the HTMLME object (and such dispatches end this test.)
        // Debug note: commenting out the next line demonstrates with debug logging that HTMLME+MSE is collected,
        // followed eventually by a timeout.
        test_scope.video.src = "";

        test_scope.expect_sourceclose = true;
        test_scope.expect_error = true;
        test_scope.video = null;
        gc();
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource preserves at least MediaSource and HTMLME and their wrappers when no references held by JS, but there is a pending event on each of HTMLME and MediaSource");

async_test(function(test) {
    var test_scope = new TestScope();

    var handler_setup_cb = null;

    var setup_done_cb = test.step_func(function() {
        var source_buffer = test_scope.media_source.addSourceBuffer('video/webm; codecs="vp8"');
        test_scope.source_buffer_list = test_scope.media_source.sourceBuffers;
        source_buffer.custom_test_wrapper_update = "testing-sourcebuffer";
        assert_true(source_buffer === test_scope.source_buffer_list[0]);

        assert_equals(test_scope.media_source.readyState, "open", "MediaSource object is open before gc().");
        assert_equals(test_scope.video.src, test_scope.object_url, "HTMLME src attribute is correct before gc().");

        source_buffer = null;
        test_scope.media_source = null;
        test_scope.video = null;


        // |test_scope.source_buffer_list| should be the only remaining reference to HTMLME+MSE+SBL+SB.
        // GC shouldn't collected this group due to that live reference.

        // Debug note: setting test_scope.source_buffer_list to null here, and commenting out the assert_equals()
        // and test.done() lines, below, demonstrates with debug logging that HTMLME+MSE is collected, followed
        // eventually by a timeout.
        gc();

        assert_equals(test_scope.source_buffer_list.length, 1, "SBL should survive gc().");
        assert_equals(test_scope.source_buffer_list[0].custom_test_wrapper_update, "testing-sourcebuffer", "SBL[0]'s wrapper should survive gc().");
        test.done();
    });

    setup_htmlme_mse_lifetime_test(test, test_scope, handler_setup_cb, setup_done_cb)
}, "GC of HTMLME+MediaSource+SBL+SB preserves at least SBL+SB when only the SourceBufferList reference is held by JS");

// TODO(wolenetz): Consider further refactoring to extract specific testing concerns from the following test:
async_test(function(test) {
    var video = document.createElement("video");
    var media_source = new MediaSource();
    var object_url = URL.createObjectURL(media_source);
    var malformed_media_append_started = false;

    video.onerror = test.step_func(function() {
        assert_true(malformed_media_append_started, "error should occur after append of malformed media bytestream started");
        test.done();
    });

    media_source.onsourceopen = test.step_func(function() {
        URL.revokeObjectURL(object_url);
        var source_buffer = media_source.addSourceBuffer('video/webm; codecs="vp8"');
        var source_buffer_list = media_source.sourceBuffers;
        assert_true(source_buffer === source_buffer_list[0]);
        source_buffer = null;
        media_source = null;
        video = null;
        // source_buffer_list's reference by us is the only thing keeping HTMLME+MSE alive.
        gc();

        setTimeout(test.step_func(function() {
            gc();
            source_buffer_list[0].onupdatestart = test.step_func(function() { malformed_media_append_started = true; });

            // Begin asynchronous append of a malformed media bytestream which
            // should result eventually with HTMLME error event firing due to the MSE
            // Append Error Algorithm.
            source_buffer_list[0].appendBuffer(new Uint8Array(10));

            // There should be a pending updatestart event on the SourceBuffer, so even if we drop the reference
            // to the SourceBufferList here, gc() still shouldn't collect HTMLME+MSE due to pending event dispatch
            // on MSE.  This is verified by HTMLME eventually handling an error event due to the asynchronous append
            // of malformed media.
            source_buffer_list = null;
            gc();
        }), 0);
    });

    video.src = object_url;

    // Debugging notes: If this test times out, it's likely due to failure in
    // keeping HTMLME+MSE alive and pending event dispatch on their wrappers alive
    // through gc(); either gc() over-collected HTMLME+MSE or event dispatch was
    // perturbed by gc().
    // If the assert in video.onerror fails, it is self-explanatory.
}, "GC of HTMLME+MediaSource+SBL+SB preserves all when only SourceBufferList reference is alive, then SourceBuffer.onupdatestart and HTMLME.onerror are dispatched if they became pending during the asynchronous buffer append algorithm, of malformed bytestream, after all HTMLME+MSE references dropped");

</script>
